<?php
namespace Pinenut;

/**
 * 
 * Корневой класс
 * Наследуется реализацией хранилища, которую в свою очередь наследует уже конкретная модель
 * 
 * @author Kosh
 *
 */
use Pinenut\Model2\Adapter;
use Pinenut\Model2\Finder;
use Pinenut\Model2\Updater;
use Pinenut\Model2\Updaters;
use Pinenut\Model2\Aggregator;
use Pinenut\Di\Injectable;
use Pinenut\Model2\Embedded;

/**
 * Class Model2
 * @package Pinenut
 */
abstract class Model2 extends Embedded {
	/**
	 * @var string
	 */
	public static $collection = null;
	
	/**
	* Внедряем зависимости
	*/
	use Injectable;

	/**
	 * @var \Pinenut\Model2\Adapter[]
	 */
	static protected $_adapter = [];
	
	/**
	 * @var \Pinenut\Model2\Adapter
	 */	
	static protected $_adapterClass;

	/**
	 * @return mixed
	 */
	static function getCollectionName() {
		$class = get_called_class();
		// определяем коллекцию по умолчанию, если не определена явно
		if (null === $class::$collection) {
			return str_replace('\\', '.', $class);
		}
		return $class::$collection;
	}
	
	/**
	 * Имея имя класса создаём объект адаптераи устанавливаем 
	 * имя коллекции
	 * Такой способ инициализации даёт возможность снаружи 
	 * подменять адаптер (DI), для статических обращений (для типизации например) 
	 * используем методы класса, а когда пишем или читаем - используем объект 
	 * проициированный как нужно 
	 * 
	 * @return \Pinenut\Model2\Adapter
	 */
	static function getAdapter() {
		/** @var static|string $class */
		$class = get_called_class();
		if (empty(static::$_adapter[$class])) {
			$adapterClass = $class::$_adapterClass;
			static::$_adapter[$class] = new $adapterClass($class::getCollectionName());
		}
		
		return static::$_adapter[$class];
	}

	/**
	 * Идентификатор сущности
	 * @var string
	 */
	public $_id;

	protected $_isNew;

	/**
	 * Признак того, что текущая модель не заселена даннымии полностью
	 * @var BOOLEAN
	 */
	protected $_isPartial;

	/**
	 * @return array
	 */
	static function fields() {
		return [
			'_id'=>static::ID,
		];
	}

	/** 
	 * Список индексов с указанием полей, направлений, опшнов
	 * @return array
	 */
	function indexes() {
		return [];
	}

	/**
	 * Конструктор модели
	 * Создаётся пустышка, с новым идентификатором или же указанным
	 *
	 * @param string|null $id
	 * @throws \Exception
	 */
	function __construct($id = null) {
		// внедряем зависимости
		$this->di();

		if (1 == rand(0, \Application::isDebug()?2:1000)) {
			$this->applyIndexes();
		}

		if (null === $id) {
			// новая запись, генерим идентификатор
			$this->_isNew = true;
			$class = static::$_adapterClass;

			if ($class) {
				$id = $class::generateId();
			} else {
				throw new \Exception('Не указан класс адаптера. Скорей всего забыли внедрить зависимости');
			}

			$this->populate(['_id'=>$id], false);
				
		} else {
			$this->_isNew = false;
			$this->_isPartial = true;
			$this->populate(['_id'=>$id], true);
//			$this->_id = $id;
		}
	}

	/**
	 * @param null $isNew
	 * @return bool|null
	 */
	public function isNew($isNew = null) {
		if (null !== $isNew) {
			$this->_isNew = $isNew;
		}
		return $this->_isNew;
	}

	/**
	 * @param null $isPartial
	 * @return bool|null
	 */
	public function isPartial($isPartial = null) {
		if (null !== $isPartial) {
			$this->_isPartial = $isPartial;
		}
		return $this->_isPartial;
	}
	
	/**
	 * Сохраняем сущность
	 */
	function save() {
		
		$this->roundEmbedded('beforeSave');

		$updaters = $this->updaters([]);

		// группируем модификаторы по условию
		// исполняем
		foreach ($updaters as $updater) {
			/* @var $updater \Pinenut\Model2\Updater */ 
			// тут я устанавливаю рав-данные, если есть, чтобы можно было 
			// сбросить более точные тэги
			$updater->execute();
		}

		$this->_isNew = false;
		
		// пробегаемся по всему дереву опять исполняя afterSave
		$this->roundEmbedded('afterSave');			
	}

	/**
	 * Для обновления всей сущности используем обновляторы
	 * из базовой реализации (вложенного документа), но внедряем
	 * в них дополнительное условие работы определяющее сущность
	 * с которой работаем по первичному ключу
	 *
	 * @see \Pinenut\Model2\Embedded::updaters()
	 * @param array $fields
	 * @return Updaters
	 */
	protected function updaters(array $fields = []) {
		$isNew = $this->isNew();
		// нужно добавить в условие идентификатор сущности
		$updaters = parent::updaters($fields);
		$updaters2 = new Updaters();
		foreach ($updaters as $updater) {
			$updater->applyFieldNames();
			$hash = $updater->hash();
			
			if (!isset($updaters2[$hash])) {
				$updaters2[$hash] = $updater;
			} else {
				$updaters2[$hash]->merge($updater);
			}

			if ($isNew) {
				$updaters2[$hash]->set('_id', $this->_id);
				$updaters2[$hash]->insert = true;
			} else {
				$updaters2[$hash]->where('_id', $this->_id);
				//$updaters2[$hash]->upsert = true;
			}
			
			//$updaters2[$hash]->setModel(get_called_class());
			$updaters2[$hash]->setModel($this); // для корректной работы 
												// мультиязычности и кеша - 
												// нужен объект-модель, который
												// будет модифицироваться
		}

		return $updaters2;
	}

	/**
	 * @return string
	 */
	function __toString() {
		return (string)$this->_id;
	}
	
	/**
	 * Выборка сущностей этого класса, 
	 * или если задан идентификатор - одного элемента,
	 * 
	 * @param string $id
	 * @param bool $active
	 * @return \Pinenut\Model2|\Pinenut\Model2\Finder
	 */
	public static function find($id = null, $active = true) {
		$finder = new Finder(get_called_class());
		$finder->active($active);
		
		if (null === $id) {
			return $finder;
		}

		$finder->where('_id', $id);
		return is_array($id) ? $finder->all() : $finder->one();
	}
	
	/**
	 * Формируем обновлятор для этой модели
	 * 
	 * @param string $id
	 * @return \Pinenut\Model2\Updater
	 */
	public static function update($id = null) {
		$updater = new Updater(get_called_class());

		if (null !== $id) {
			$updater->where('_id', $id);
		}

		return $updater;
	}
	
	/**
	 * Формируем удалятор для удаления модели
	 *
	 * @param string $id
	 * @return \Pinenut\Model2\Updater
	 */
	public static function delete($id = null) {
		$deleter = new Updater(get_called_class());

		if (null !== $id) {
			$deleter->where('_id', $id);
		}

		return $deleter->delete();
	}
	
	/**
	 * Формируем агрегатор для этой модели
	 *
	 * @param string $id
	 * @return \Pinenut\Model2\Aggregator
	 */
	public static function aggregate($id = null) {
		$aggregate = new Aggregator(get_called_class());

		if (null !== $id) {
			$aggregate->where('_id', $id);
		}

		return $aggregate;
	}
	
	/**
	 * Удалятор записи с дерганьем вложеннных документов
	 */
	public function remove () {
		$this->roundEmbedded('beforeRemove');
		static::delete($this->_id)->execute();
		$this->roundEmbedded('afterRemove');
	}
	
	/**
	 * Вешаем индексы
	 * пока по-быстрому тупо-все, потом надо будет 
	 * чекать существующие
	 */
	public function applyIndexes() {
		$adapter = static::getAdapter();
		$indexes = $this->getIndexes();

		foreach ($indexes as $name=>$fields) {
			$adapter->ensureIndex($fields, $name);
		}
	}

	/**
	 * Получаем индексы
	 */
	public function getIndexes()
	{
		return $this->indexes();
	}

	/**
	 * @param Finder $finder
	 */
	protected static function onBeforeFetch(Finder &$finder)
	{

	}

	/**
	 * @param $raw
	 * @param Finder $finder
	 */
	protected static function onAfterFetch(&$raw, Finder &$finder)
	{

	}

	/**
	 * @param $model
	 * @param Finder $finder
	 */
	protected static function onAfterFetchPopulate($model, Finder $finder)
	{

	}

	/**
	 * @param Finder $finder
	 * @param bool $active
	 * @param bool $count
	 * @return array
	 */
	public static function fetch(Finder $finder, $active = true, $count = false) {
		/** @var Adapter|Aggregator $adapter */
		$adapter = static::getAdapter();

		static::onBeforeFetch($finder);

		if ($count) {
			// если каунт от и так всё понятно
			return $adapter->count ($finder->getWhere());
		}
		
		if ($finder instanceof Aggregator) {
			// агрегатор
			return $adapter->aggregate(
				$finder->getWhere(),
				$finder->getFields(),
				$finder->getGroupBy(),
				$finder->getOperators()
			);
		}

		// если полновесный запрос от делаем селект,
		// а потом генерим объекты если нужно
		$raws = $adapter->select (
			$finder->getWhere(),
			$finder->getFields(),
			$finder->getOrder(),
			$finder->getOffset(),
			$finder->getLimit()
		);

		if (empty($raws)) {
			return null;
		}

		static::onAfterFetch($raws, $finder);

		if (!$active) {
			return $raws;
		}

		$objects = [];
		foreach ( $raws as $raw ) {
			$object = new static ();
			/* @var \Pinenut\Model2 $object */
			$object->isNew(false);
			$object->isPartial(false);
			$object->populate ( $raw );
			static::onAfterFetchPopulate($object, $finder);
			$objects [] = $object;
		}
		return $objects;
	}
	
	
	/**
	 * Модифицируем апдейтер как нам нужно до execute
	 * @param Updater $updater
	 */
	protected static function onBeforeExecute(Updater &$updater) {

	}

	/**
	 * Модифицируем апдейтер как нам нужно после execute
	 * @param Updater $updater
	 */
	protected static function onAfterExecute(Updater &$updater) {

	}
	

	/**
	 * @param Updater $updater
	 */
	public static function execute(Updater $updater) {
		$adapter = static::getAdapter();
		static::onBeforeExecute($updater);
		if ($updater->isInsert()) {
			$ops = $updater->getOperators();
			if (isset($ops[Updater::OP_SET]) && !empty($ops[Updater::OP_SET])) {
				$adapter->insert($ops[Updater::OP_SET]);
			}
		} elseif ($updater->isDelete()) {
			$adapter->remove($updater->getWhere());
		} else {
			$adapter->update($updater->getWhere(), 
				$updater->getOperators());//, $updater->upsert);
		}
		static::onAfterExecute($updater);
	}
}